Thunk (object-oriented programming)

Some compilers for object-oriented languages such as C++ generate functions called thunks as an optimization of virtual function calls in the presence of multiple or virtual inheritance. Consider the C++ code

struct A {
    int value;
    virtual int access() { return this->value; }
};
struct B {
    int value;
    virtual int access() { return this->value; }
};
struct C : public A, public B {
    int better_value;
    virtual int access() { return this->better_value; }
};
 
int use(B *b)
{
    return b->access();
}
 
... C c; use(&c); ...

Since the function B::access is virtual, a call to b->access() requires use of a vtable dispatch. In naïve implementations, the dispatch will consist of five steps:[1]:section 5.1

  1. The object pointed by b holds a pointer to the vtable. Load that pointer into a register.
  2. The vtable for class B contains an entry (dispatch) for the method B::access. Find that entry E.
  3. E contains a pointer to a function (in this case, the method C::access). Load that function pointer.
  4. The method C::access expects a this pointer to an instance of class C. But b points to an instance of class B. So we must decrement b by the offset of B in C (in this example, by the size of C::A::value plus the size of C::A's vtable pointer). Since this offset is not known to the method use at compile time, it must also be loaded from the vtable entry E.
  5. Finally, call C::access with the adjusted value of b.

The fourth step, in which an offset (a negative offset in this example) is loaded from E and added to b, can be completely eliminated by the compiler, thus speeding up every virtual method call, if the compiler generates a wrapper function like this, and places its address in the vtable entry E:

int thunk_for_C_access_in_B(B *b)
{
    C *adjusted_b = (C *)b;          /* decrements b by the appropriate offset,
                                        so that it points to a C object */
    return adjusted_b->C::access();  /* a tail call to the original method C::access() */
}

Then the steps for b->access() become:

  1. The object pointed by b holds a pointer to the vtable. Load that pointer into a register.
  2. The vtable entry for B::access is at some known offset in the vtable for B; find that entry E.
  3. E contains a pointer to a function (in this case, thunk_for_C_access_in_B). Load that function pointer W.
  4. Call W with the value of b.   If b was really of dynamic type B, then W = B::access, and so we have saved two instructions (an expensive memory load and a cheap addition). If b was really of dynamic type C, then W = thunk_for_C_access_in_B, and so we have added one instruction (a cheap unconditional branch at the end of thunk_for_C_access_in_B).

Since the particular pattern of multiple inheritance in class C is rare in practice, we will generally save more instructions than we add. At the same time, we no longer need to store an offset for each entry E in the vtable, and so we have halved the size of every vtable in the program.

The term "thunk" for these compiler-generated functions can be seen as an example of "thunk", meant as a nullary function (one with no parameters).[2]:page 3 It could have been described simply as a compiler-generated wrapper function, but the term "thunk" for these functions is now established as convention.

References

  1. ^ Stroustrup, Bjarne (May 1999). Multiple Inheritance in C++. C/C++ Users Journal. http://www-plan.cs.colorado.edu/diwan/class-papers/mi.pdf. Retrieved 24 February 2011. 
  2. ^ Driesen, Karel; Hölzle, Urs (1996). The Direct Cost of Virtual Function Calls in C++. OOPSLA. http://www.cs.ucsb.edu/~urs/oocsb/papers/oopsla96.pdf. Retrieved 24 February 2011.